/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as performance from "./vs/base/common/performance.js"
import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from "./bootstrap-node.js"
import { bootstrapESM } from "./bootstrap-esm.js"

performance.mark("code/fork/start")

//#region Helpers

function pipeLoggingToParent(): void {
	const MAX_STREAM_BUFFER_LENGTH = 1024 * 1024
	const MAX_LENGTH = 100000

	/**
	 * Prevent circular stringify and convert arguments to real array
	 */
	function safeToString(args: ArrayLike<unknown>): string {
		const seen: unknown[] = []
		const argsArray: unknown[] = []

		// Massage some arguments with special treatment
		if (args.length) {
			for (let i = 0; i < args.length; i++) {
				let arg = args[i]

				// Any argument of type 'undefined' needs to be specially treated because
				// JSON.stringify will simply ignore those. We replace them with the string
				// 'undefined' which is not 100% right, but good enough to be logged to console
				if (typeof arg === "undefined") {
					arg = "undefined"
				}

				// Any argument that is an Error will be changed to be just the error stack/message
				// itself because currently cannot serialize the error over entirely.
				else if (arg instanceof Error) {
					const errorObj = arg
					if (errorObj.stack) {
						arg = errorObj.stack
					} else {
						arg = errorObj.toString()
					}
				}

				argsArray.push(arg)
			}
		}

		try {
			const res = JSON.stringify(argsArray, function (key, value: unknown) {
				// Objects get special treatment to prevent circles
				if (isObject(value) || Array.isArray(value)) {
					if (seen.indexOf(value) !== -1) {
						return "[Circular]"
					}

					seen.push(value)
				}

				return value
			})

			if (res.length > MAX_LENGTH) {
				return "Output omitted for a large object that exceeds the limits"
			}

			return res
		} catch (error) {
			return `Output omitted for an object that cannot be inspected ('${error.toString()}')`
		}
	}

	function safeSend(arg: { type: string; severity: string; arguments: string }): void {
		try {
			if (process.send) {
				process.send(arg)
			}
		} catch (error) {
			// Can happen if the parent channel is closed meanwhile
		}
	}

	function isObject(obj: unknown): boolean {
		return (
			typeof obj === "object" &&
			obj !== null &&
			!Array.isArray(obj) &&
			!(obj instanceof RegExp) &&
			!(obj instanceof Date)
		)
	}

	function safeSendConsoleMessage(severity: "log" | "warn" | "error", args: string): void {
		safeSend({ type: "__$console", severity, arguments: args })
	}

	/**
	 * Wraps a console message so that it is transmitted to the renderer.
	 *
	 * The wrapped property is not defined with `writable: false` to avoid
	 * throwing errors, but rather a no-op setting. See https://github.com/microsoft/vscode-extension-telemetry/issues/88
	 */
	function wrapConsoleMethod(method: "log" | "info" | "warn" | "error", severity: "log" | "warn" | "error"): void {
		Object.defineProperty(console, method, {
			set: () => {},
			get: () =>
				function () {
					safeSendConsoleMessage(severity, safeToString(arguments))
				},
		})
	}

	/**
	 * Wraps process.stderr/stdout.write() so that it is transmitted to the
	 * renderer or CLI. It both calls through to the original method as well
	 * as to console.log with complete lines so that they're made available
	 * to the debugger/CLI.
	 */
	function wrapStream(streamName: "stdout" | "stderr", severity: "log" | "warn" | "error"): void {
		const stream = process[streamName]
		const original = stream.write

		let buf = ""

		Object.defineProperty(stream, "write", {
			set: () => {},
			get:
				() =>
				(
					chunk: string | Buffer | Uint8Array,
					encoding: BufferEncoding | undefined,
					callback: ((err?: Error | undefined) => void) | undefined,
				) => {
					buf += chunk.toString(encoding)
					const eol = buf.length > MAX_STREAM_BUFFER_LENGTH ? buf.length : buf.lastIndexOf("\n")
					if (eol !== -1) {
						console[severity](buf.slice(0, eol))
						buf = buf.slice(eol + 1)
					}

					original.call(stream, chunk, encoding, callback)
				},
		})
	}

	// Pass console logging to the outside so that we have it in the main side if told so
	if (process.env["VSCODE_VERBOSE_LOGGING"] === "true") {
		wrapConsoleMethod("info", "log")
		wrapConsoleMethod("log", "log")
		wrapConsoleMethod("warn", "warn")
		wrapConsoleMethod("error", "error")
	} else {
		console.log = function () {
			/* ignore */
		}
		console.warn = function () {
			/* ignore */
		}
		console.info = function () {
			/* ignore */
		}
		wrapConsoleMethod("error", "error")
	}

	wrapStream("stderr", "error")
	wrapStream("stdout", "log")
}

function handleExceptions(): void {
	// Handle uncaught exceptions
	process.on("uncaughtException", function (err) {
		console.error("Uncaught Exception: ", err)
	})

	// Handle unhandled promise rejections
	process.on("unhandledRejection", function (reason) {
		console.error("Unhandled Promise Rejection: ", reason)
	})
}

function terminateWhenParentTerminates(): void {
	const parentPid = Number(process.env["VSCODE_PARENT_PID"])

	if (typeof parentPid === "number" && !isNaN(parentPid)) {
		setInterval(function () {
			try {
				process.kill(parentPid, 0) // throws an exception if the main process doesn't exist anymore.
			} catch (e) {
				process.exit()
			}
		}, 5000)
	}
}

function configureCrashReporter(): void {
	const crashReporterProcessType = process.env["VSCODE_CRASH_REPORTER_PROCESS_TYPE"]
	if (crashReporterProcessType) {
		try {
			//@ts-ignore
			if (
				process["crashReporter"] &&
				typeof process["crashReporter"].addExtraParameter === "function" /* Electron only */
			) {
				//@ts-ignore
				process["crashReporter"].addExtraParameter("processType", crashReporterProcessType)
			}
		} catch (error) {
			console.error(error)
		}
	}
}

//#endregion

// Crash reporter
configureCrashReporter()

// Remove global paths from the node module lookup (node.js only)
removeGlobalNodeJsModuleLookupPaths()

if (process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"]) {
	devInjectNodeModuleLookupPath(process.env["VSCODE_DEV_INJECT_NODE_MODULE_LOOKUP_PATH"])
}

// Configure: pipe logging to parent process
if (!!process.send && process.env["VSCODE_PIPE_LOGGING"] === "true") {
	pipeLoggingToParent()
}

// Handle Exceptions
if (!process.env["VSCODE_HANDLES_UNCAUGHT_ERRORS"]) {
	handleExceptions()
}

// Terminate when parent terminates
if (process.env["VSCODE_PARENT_PID"]) {
	terminateWhenParentTerminates()
}

// Bootstrap ESM
await bootstrapESM()

// Load ESM entry point
await import(
	[`./${process.env["VSCODE_ESM_ENTRYPOINT"]}.js`].join(
		"/",
	) /* workaround: esbuild prints some strange warnings when trying to inline? */
)
